/**
 * Copyright (c) 2006-2013 Las Venturas Mineground
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package org.mineground.events;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;

import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Event Dispatcher is in charge of both maintaining a registry with the registered event
 * listeners, as well as actually dispatching the events which we receive from Bukkit.
 * 
 * The mEventListeners member is a hash map for which the key is the type of a class which inherits
 * Bukkit's Event class. Each entry's value is a sorted set, implemented through a concurrent
 * skip list set (sorted on priority, and we may access it concurrently) containing a weak reference
 * to the object which we'll invoke the listener on. This reference has to be weak, as we do not
 * want to keep the object alive is all other references threw it away.
 */
public class EventDispatcher {
    private static final Map<Class<? extends Event>, SortedSet<EventListenerReference>>
            mEventListeners = new HashMap<Class<? extends Event>, SortedSet<EventListenerReference>>();
    
    private static final Logger MessageLogger = LoggerFactory.getLogger(EventDispatcher.class);

    /**
     * Register an individual method with the event listening registry. The type of event will be
     * derived from the type of the first parameter, which must extend Bukkit's Event type. We'll
     * only keep weak references to the registered events, so no unregister method is necessary.
     * 
     * @param object Instance of the class which owns this listener, will be weakly stored.
     */
    public static void RegisterListener(EventListenerRegistrar object) {
        final Class<?> objectClass = object.getClass();
        try {
            for (Method declaredMethod : objectClass.getDeclaredMethods()) {
                if (declaredMethod.isAnnotationPresent(EventListener.class) == false)
                    continue;

                final EventListener listenerAnnotation = declaredMethod.getAnnotation(EventListener.class);
                final Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
                if (parameterTypes.length != 1) {
                    MessageLogger.warn("Method " + objectClass.getName() + "::" + declaredMethod.getName() + "() refused " +
                            "for event listener registration: invalid parameter count.");

                    continue; 
                }

                final Class<? extends Event> eventType = parameterTypes[0].asSubclass(Event.class);
                if (mEventListeners.get(eventType) == null)
                    mEventListeners.put(eventType, new ConcurrentSkipListSet<EventListenerReference>());

                mEventListeners.get(eventType).add(new EventListenerReference(object, declaredMethod,
                        listenerAnnotation.value()));
            }
        } catch (SecurityException ex) {
            MessageLogger.warn("Class " + objectClass.getName() + " refused for event listener registration: " +
                    "class reflection prohibited by a security policy.");
        }
    }
    
    /**
     * Invoking this method with any given Bukkit Event will distribute in order of the priorities
     * registered listeners have. Listeners with a high priority will be called first, and once an
     * event has been cancelled further listeners will not be invoked anymore.
     * 
     * Worst-case complexity of this method is O(n), in which "n" is the number of event listeners
     * registered for the given type. Retrieving the Set containing the actual listeners is O(1) due
     * to the nature of hash maps.
     * 
     * When no events are registered for the given event type, we'll bail out immediately. This is,
     * however, a valid case we do not have to log for. Handlers for this event type may be added
     * later on during Las Venturas Mineground's runtime.
     * 
     * @param event The event which has to be dispatched within the Mineground plugin.
     */
    public static <Type extends Event> void DispatchEvent(Type event) {
        final Class<?> eventClass = event.getClass();
        if (mEventListeners.get(eventClass) == null)
            return;
        
        final boolean isCancellable = Cancellable.class.isAssignableFrom(eventClass);
        final Iterator<EventListenerReference> referenceIterator = mEventListeners.get(eventClass).iterator();

        while (referenceIterator.hasNext()) {
            EventListenerReference reference = referenceIterator.next();

            Object instance = reference.mObjectReference.get();
            if (instance == null) {
                referenceIterator.remove();
                if (mEventListeners.get(eventClass).size() == 0)
                    mEventListeners.remove(eventClass);

                continue;
            }
            
            try {
                reference.mListener.invoke(instance, event);
                
                // If this event type is a cancellable event, which most events dispatched by Bukkit
                // are, then check if it has been cancelled. If so, don't process other listeners.
                if (isCancellable && ((Cancellable) event).isCancelled())
                    break;

            } catch (Exception e) {
                // TODO: Should we unregister a method after several failed invocations?
                MessageLogger.warn("Invocation of " + instance.getClass().getName() + "::" + reference.mListener.getName() +
                        "() failed due to an exception (" + e.getClass().getName() + "): " + e.getMessage());
            }
        }
    }
    
    /**
     * Returns whether we have any registered listeners for the given event type.
     * 
     * @param eventClass The Class<?> type of the event to check listeners for.
     * @return Whether we currently have any listeners for the given event type.
     */
    public static boolean HasListenersForEventType(Class<? extends Event> eventClass) {
        return mEventListeners.get(eventClass) != null;
    }
}
